<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>研究滑块拼图的破解方案</title>
    <meta name="description" content="通过灰度化将颜色分为：透明色和0~3阶灰度，然后比较区域内色阶的相似程度">
    <style>
        html, body {
            padding: 0;
            margin: 0;
        }

        body {
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        #canvasBox {
            margin: 12px auto;
        }

        #canvasBox canvas {
            display: block;
            margin: 12px 12px 0 12px;
        }
    </style>
</head>
<body>
    <div style="margin: 12px auto">
        <input id="openJigsawJson" type="file" multiple>
    </div>
    <div id="canvasBox"></div>

    <script>
        (async () => {
            function addP(content, parent) {
                const p = document.createElement("p");
                p.innerHTML = content;
                (parent || document.body).appendChild(p);
            }

            async function createImgCanvas(imgInfo) {
                return new Promise((resolve, reject) => {
                    const img = document.createElement("img");
                    img.onload = () => {
                        const canvas = document.createElement("canvas");
                        canvas.width = imgInfo.width;
                        canvas.height = imgInfo.height;
                        const context = canvas.getContext("2d");
                        context.drawImage(img, 0, 0, canvas.width, canvas.height);
                        resolve([canvas, context]);
                    };
                    img.onerror = err => {
                        reject(err);
                    };
                    img.src = imgInfo.src;
                });
            }

            async function analysis(jigsawInfo, fileName, clear) {
                const offsetL = jigsawInfo.front.left - jigsawInfo.back.left;
                const offsetT = jigsawInfo.front.top - jigsawInfo.back.top;

                const [frontCanvas, frontContext] = await createImgCanvas(jigsawInfo.front);
                const [backCanvas, backContext] = await createImgCanvas(jigsawInfo.back);
                const [frontCanvasForDebug, frontContextForDebug] = await createImgCanvas(jigsawInfo.front);
                const [backCanvasForDebug, backContextForDebug] = await createImgCanvas(jigsawInfo.back);
                const canvasBox = document.getElementById("canvasBox");
                if (clear) {
                    for (let child of Array.from(canvasBox.children)) {
                        canvasBox.removeChild(child);
                    }
                }
                addP(fileName, canvasBox);
                canvasBox.appendChild(frontCanvasForDebug);
                canvasBox.appendChild(backCanvasForDebug);
                backCanvasForDebug.style.marginBottom = "36px";

                {
                    // start
                    const to0123 = (imageData, front0123) => {
                        const grays = [];
                        let grayAvg = 0;
                        for (let i = 0, len = imageData.length; i < len; i += 4) {
                            const color = imageData.subarray(i, i + 4);
                            if (color[3] < 255 || (front0123 && front0123[Math.floor(i / 4)] === -1)) {
                                // 透明区域不参加对比
                                grays.push(-1);
                            }
                            else {
                                const gray = color[0] * 0.3 + color[1] * 0.59 + color[2] * 0.11;
                                grays.push(gray);
                                grayAvg += gray;
                            }
                        }
                        grayAvg /= grays.length;
                        let grayAvgLower = 0;
                        let grayAvgLowerNum = 0;
                        let grayAvgUpper = 0;
                        let grayAvgUpperNum = 0;
                        for (let item of grays) {
                            if (item === -1) {

                            }
                            else if (item < grayAvg) {
                                grayAvgLower += item;
                                grayAvgLowerNum++;
                            }
                            else {
                                grayAvgUpper += item;
                                grayAvgUpperNum++;
                            }
                        }
                        grayAvgLower /= grayAvgLowerNum || 1;
                        grayAvgUpper /= grayAvgUpperNum || 1;
                        return grays.map(item => item === -1 ? -1 : (item < grayAvgLower ? 0 : (item < grayAvg ? 1 : (item < grayAvgUpper ? 2 : 3))));
                    };

                    const sameOf0123 = (arr1, arr2) => {
                        return arr1.map((item, index) => item === -1 ? 0 : 1 - Math.abs(item - arr2[index]) * 0.8).reduce((preV, curV) => preV + curV);
                    };

                    // 非透明色的个数
                    const notTransparentNum = colors => {
                        let res = 0;
                        for (let i = 3; i < colors.length; i += 4) {
                            if (colors[i] >= 200) {
                                res++;
                            }
                        }
                        return res;
                    };

                    // 色阶转换，用于debug
                    const colorLevels = [
                        [0, 187, 255, 255],
                        [0, 255, 136, 255],
                        [216, 255, 0, 255],
                        [255, 0, 95, 255],
                    ];
                    const translateColorLevels = (imageData, color0123) => {
                        color0123.forEach((value, index) => {
                            if (value > -1) {
                                imageData.set(colorLevels[value], index * 4);
                            }
                        });
                    };

                    let maskEdgeL = null;
                    let maskEdgeR = null;
                    let maskEdgeT = null;
                    let maskEdgeB = null;
                    let maskEdgeMargin = 3;

                    for (let x = 0; x < frontCanvas.width; x++) {
                        const maskColors = frontContext.getImageData(x, 0, 1, frontCanvas.height).data;
                        if (notTransparentNum(maskColors) > 20) {
                            if (maskEdgeL == null) {
                                maskEdgeL = x;
                            }
                            maskEdgeR = x;
                        }
                    }

                    for (let y = 0; y < frontCanvas.height; y++) {
                        const maskColors = frontContext.getImageData(0, y, frontCanvas.width, 1).data;
                        if (notTransparentNum(maskColors) > 20) {
                            if (maskEdgeT == null) {
                                maskEdgeT = y;
                            }
                            maskEdgeB = y;
                        }
                    }
                    maskEdgeL += maskEdgeMargin;
                    maskEdgeR -= maskEdgeMargin;
                    maskEdgeT += maskEdgeMargin;
                    maskEdgeB -= maskEdgeMargin;

                    let bestSame = 0;
                    let bestDelta = backCanvas.width - (maskEdgeR - maskEdgeL) - 12;
                    const frontImageData = frontContext.getImageData(maskEdgeL, maskEdgeT, maskEdgeR - maskEdgeL, maskEdgeB - maskEdgeT).data;
                    const front0123 = to0123(frontImageData, null);
                    for (let xDelta = 25; xDelta < backCanvas.width - 25; xDelta++) {
                        const backImageData = backContext.getImageData(maskEdgeL + xDelta, maskEdgeT + offsetT, maskEdgeR - maskEdgeL, maskEdgeB - maskEdgeT).data;
                        const back0123 = to0123(backImageData, front0123);
                        const same = sameOf0123(front0123, back0123);
                        if (bestSame <= same) {
                            bestSame = same;
                            bestDelta = xDelta;
                        }
                    }

                    if (frontCanvasForDebug) {
                        frontContextForDebug.fillStyle = `rgb(255, 128, 83)`;
                        frontContextForDebug.fillRect(maskEdgeL, 0, 1, frontCanvasForDebug.height);
                        frontContextForDebug.fillRect(maskEdgeR, 0, 1, frontCanvasForDebug.height);
                        frontContextForDebug.fillRect(0, maskEdgeT, frontCanvasForDebug.width, 1);
                        frontContextForDebug.fillRect(0, maskEdgeB, frontCanvasForDebug.width, 1);

                        const imageData = frontContextForDebug.getImageData(maskEdgeL, maskEdgeT, maskEdgeR - maskEdgeL, maskEdgeB - maskEdgeT);
                        translateColorLevels(imageData.data, front0123);
                        frontContextForDebug.putImageData(imageData, maskEdgeL, maskEdgeT);
                    }

                    if (backContextForDebug) {
                        backContextForDebug.fillStyle = `rgb(255, 128, 83)`;
                        backContextForDebug.fillRect(maskEdgeL + bestDelta, 0, 1, backCanvasForDebug.height);
                        backContextForDebug.fillRect(maskEdgeR + bestDelta, 0, 1, backCanvasForDebug.height);
                        backContextForDebug.fillRect(0, maskEdgeT + offsetT, backCanvasForDebug.width, 1);
                        backContextForDebug.fillRect(0, maskEdgeB + offsetT, backCanvasForDebug.width, 1);

                        const backImageData = backContext.getImageData(maskEdgeL + bestDelta, maskEdgeT + offsetT, maskEdgeR - maskEdgeL, maskEdgeB - maskEdgeT);
                        const back0123 = to0123(backImageData.data, front0123);
                        translateColorLevels(backImageData.data, back0123);
                        backContextForDebug.putImageData(backImageData, maskEdgeL + bestDelta, maskEdgeT + offsetT);
                    }

                    return bestDelta - offsetL;
                    // end
                }
            }

            document.getElementById("openJigsawJson").onchange = event => {
                if (event.target.files && event.target.files.length) {
                    Array.from(event.target.files).forEach((file, index) => {
                        const fileReader = new FileReader();
                        fileReader.onload = (err, res) => {
                            const info = JSON.parse(fileReader.result);
                            analysis(info, file.name, index === 0);
                        };
                        fileReader.readAsBinaryString(file);
                    });
                }
            };
        })();
    </script>
</body>
</html>